<head>
  <meta charset="UTF-8">

  <style type="text/css">
    .note {
        color: #a0a0a0;
        font-size: 12px;
        font-style: italic;
    }

    table.callStackTable {
        width:100%;
    }
    table.callStackTable tr {
        background-color: #f0f0f0;
    }

    .prototype-warning {
        background-color:#ffb0b0; color:#ff0000; padding:8px; border:1px solid red; display:inline-block;
    }

    .prototype-warning-small {
        background-color:#ffb0b0; color:#ff0000; padding:8px; border:1px solid red; display:inline-block;
        font-size:12px;
    }

    table tr {
        padding: 0;
    }

    table td {
        padding: 0;
    }

  </style>

  <script src="binary_buffer.js"></script>
  <script src="branch_trace_reader.js"></script>
  <script src="branch_profile.js"></script>

  <!-- Plotly.js -->
  <script src="plotly-latest.min.js"></script>
  
  <link rel="stylesheet" href="jquery-ui-1.12.1.custom/jquery-ui.css">
  <script src="jquery-3.4.1.min.js"></script>
  <script src="jquery-ui-1.12.1.custom/jquery-ui.js"></script>


  <link rel="stylesheet" href="ion-rangeslider/2.3.0/css/ion.rangeSlider.min.css"/>    
  <script src="ion-rangeslider/2.3.0/js/ion.rangeSlider.min.js"></script>


  <script src="http://culmat.github.io/jsTreeTable/treeTable.js"></script>

  <script>

    let SNAPSHOT_TOKEN = 1
    let POINT_TOKEN = 0

    var DEBUG = true // disable to stop debug messages
    
    var tablexdim = 1024
    var numBanks = 2
    var numTables = 16

    var xdim = tablexdim
    var ydim = numTables * numBanks

    var tStart = 0
    var tEnd = 5000000;
    var dbgEl = null;

    var currentFile = null // Currently opened file (null is unintialized state orrandom data)

    var trackedCells = Array()
    var branchFilters = new Map() // branches being tracked specifically

    var statIndex = 0
    var statIndexMap = {
        "accesses": 0,
        "weight_change": 1,
        "unique_pcs": 4,
        "thrash_1": 5,
        "weight_change_mag": 3,
        "weight_mpreds": 10,
        "weight_cpreds": 9,
        "contra_mispred": 8,
        "contrib_mispred": 7,
        "weight_accuracy": -1,
        "weight_change_events": -2,
        "mp_harm_rate": -3,
        "mp_help_rate": -4,
        "mispred": -5,
        "weight_change_thrash": -6
    }
    var statIndexReverseMap = {}
    for (let k in statIndexMap) {
        statIndexReverseMap[statIndexMap[k]] = k
    }

    function logDebug(f) {
        if (!DEBUG)
            return;

        if (!dbgEl) {
            alert('unable to log debug because debug msg has not been set yet:' + f());
            return;
        }
        
        dbgEl.innerHTML = f() + "<br/>" + dbgEl.innerHTML.slice(0,1000);
    }

    // Indices along heatmap x and y axes
    var xValues = [...Array(xdim).keys()];
    var yValues = [...Array(ydim).keys()];

    var colorscaleValue = [
      [0, '#3D9970'],
      [1, '#001f3f']
    ];

    var colorScaleGradientBlueRed = [
        [0.0, 'rgb(0,0,255)'],
        [0.1, 'rgb(49,54,149)'],
        [0.2, 'rgb(69,117,180)'],
        [0.3, 'rgb(116,173,209)'],
        [0.4, 'rgb(171,217,233)'],
        [0.5, 'rgb(224,243,248)'],
        [0.6, 'rgb(254,224,144)'],
        [0.7, 'rgb(253,174,97)'],
        [0.8, 'rgb(244,109,67)'],
        [0.9, 'rgb(215,48,39)'],
        [1.0, 'rgb(255,0,0)'],
        ]

    var colorScaleGradientBlueGreenRed = [
        [0.0, 'rgb(180,180,255,0.3)'],
        [0.1, 'rgb(50,90,240,0.3)'],
        [0.2, 'rgb(50,110,220,0.3)'],
        [0.3, 'rgb(50,180,125,0.3)'],
        [0.4, 'rgb(50,190,100,0.3)'],
        [0.5, 'rgb(50,200,20,0.3)'],
        [0.6, 'rgb(100,200,0,0.3)'],
        [0.7, 'rgb(180,200,0,0.5)'],
        [0.8, 'rgb(255,200,0,0.7)'],
        [0.9, 'rgb(255,100,0,0.9)'],
        [1.0, 'rgb(255,0,0,1.0)'],
        ]

    // Logarithmic scale
    var colorScaleLogGradientBlueRed = [
        [0.0, 'rgb(0,0,255,0.6)'],
        [Math.log(1) / Math.log(10), 'rgb(49,54,149,0.6)'],
        [Math.log(2) / Math.log(10), 'rgb(69,117,180,0.6)'],
        [Math.log(3) / Math.log(10), 'rgb(116,173,209,0.6)'],
        [Math.log(4) / Math.log(10), 'rgb(171,217,233,0.6)'],
        [Math.log(5) / Math.log(10), 'rgb(224,243,248,0.6)'],
        [Math.log(6) / Math.log(10), 'rgb(254,224,144,0.6)'],
        [Math.log(7) / Math.log(10), 'rgb(253,174,97,0.6)'],
        [Math.log(8) / Math.log(10), 'rgb(244,109,67,0.6)'],
        [Math.log(9) / Math.log(10), 'rgb(215,48,39,0.6)'],
        [1.0, 'rgb(255,0,0,0.6)'],
        ]

    var colorScaleLogGradientBlueGreenRed = [
        [0.0,                        'rgb(0,0,255,0.6)'],
        [Math.log(1) / Math.log(10), 'rgb(49,50,149,0.6)'],
        [Math.log(2) / Math.log(10), 'rgb(37,100,112,0.6)'],
        [Math.log(3) / Math.log(10), 'rgb(24,150,75,0.6)'],
        [Math.log(4) / Math.log(10), 'rgb(11,205,34,0.6)'],
        [Math.log(5) / Math.log(10), 'rgb(0,200,0,0.6)'],
        [Math.log(6) / Math.log(10), 'rgb(85,200,12,0.6)'],
        [Math.log(7) / Math.log(10), 'rgb(170,200,24,0.6)'],
        [Math.log(8) / Math.log(10), 'rgb(255,200,37,0.6)'],
        [Math.log(9) / Math.log(10), 'rgb(255,100,49,0.6)'],
        [1.0,                        'rgb(255,0,0,0.6)'],
        ]

    var colorScaleGradient = colorScaleGradientBlueGreenRed


    let tableAxisHeight = 350

    var layout = {
      title: '',
      //annotations: [],
      //width: 500,
      height: tableAxisHeight,
      margin: { t:0, l:20, r:0, b:40},
      xaxis: {
        autorange: true,
        //autorange: false, // performance
        //range: [0, xdim], // performance
        title: 'row ()',
        ticks: '',
        side: 'bottom'
      },
      yaxis: {
        autorange: true,
        //autorange: false, // performance
        //range: [-0.5, ydim+0.5], // performance
        //title: 'table index',
        ticks: '',
        ticksuffix: ' ',
        //width: 700,
        //height: 700,
        autosize: true
      }
    };

    var tableDataLayout = {
      showlegend: false,
      title: 'Table Means',
      //annotations: [],
      //width: 500,
      height: tableAxisHeight,
      margin: { t:0, l:40, r:0, b:40},
      xaxis: {
        autorange: 'reversed',
        title: 'mean ???',
        ticks: '',
        side: 'bottom'
      },
      yaxis: {
        autorange: false, // performance
        range: [-0.5, ydim+0.5], // performance
        title: 'table index (table# x nbanks + bank#)',
        ticks: '',
        ticksuffix: ' ',
        //width: 700,
        //height: 700,
        autosize: true
      }
    };

    var rowDataLayout = {
      showlegend: false,
      title: '',
      //annotations: [],
      //width: 500,
      height: 100,
      margin: { t:25, l:20, r:90, b:0},
      xaxis: {
        visible: false,
        autorange: false,
        range: [-0.5, xdim+0.5],
        title: '',
        autotick: true,
        ticks: '',
        side: 'bottom'
      },
      yaxis: {
        autorange: true,
        title: '',
        ticks: '',
        ticksuffix: ' ',
        //width: 700,
        //height: 700,
        autosize: true
      }
    };

    function randX() {
        return Math.floor(Math.random() * xdim);
    }
    function randY() {
        return Math.floor(Math.random() * ydim);
    }

    // Generate some random timeseries data
    // ts = [ [t,x,y,w,c1,c2,c3], ...  ]
    // c1=unique accesses
    // c2=accesses
    // c3=delta weight
    // TODO: store snapshots in their OWN array for faster traversal. jumping randomly looking or steps is terrible. Js must have a good prefetcher since iteration of evrything is fast
    function generateRandomData() {
        let ts = [];
        let snapshotPeriod = 200000; // NOTE: snapshots do not appear to improve rendering performance except when 1m plus. why?

        // Note that using a flat array for the snapshot greatly improves iteration sped
        let tempZ = Array(ydim*xdim);
        tempZ.fill(0);

        let snapStart = tStart;
        let snapCount = 0
        for (var t = tStart; t < tEnd; t++) {
            x = randX();
            y = randY();
            w = Math.random()*100;
            
            // hide some high-ish frequency sinusoidal noise in upper tables. period is about 628k.
            if (y > 10) 
                c1 = Number(Math.sin(t / 100000) + 0.5 >= Math.random());
            else if (y > 5) 
                c1 = Number(Math.cos(t / 200000) + 0.5 >= Math.random());
            else
                c1 = Number(Math.random() > 0.5);
            if ((t % snapshotPeriod) == 0) {
                //logDebug(function(){return "snapshot=" + ts.length;});
                snapEnd = t;
                ts.push([SNAPSHOT_TOKEN, snapStart, snapEnd, tempZ, snapCount]);
                snapStart = t;
                tempZ = Array(ydim*xdim);
                tempZ.fill(0); // reset snapshot
                snapCount = 0

                // NOTE: skip an event this cycle just to keep each entries time matching its index for this prototype
            }
            else {
                ts.push([POINT_TOKEN, t, x, y, w,
                     c1,
                     0, // c2
                     1, // pclo
                     2, // pchi
                     t%2 // correct
                     ]);

                // TEMP: only tracking 1 counter for now. If more were needed, keep the array flat and index accordingly
                tempZ[y*xdim + x] += c1;
                snapCount++
            }
        }
        logDebug(function(){return "random data generated: ts=" + ts.length;});
        return ts;
    }

    // Load data from a binary string and populate ts,tStart,tEnd, etc.
    // Update the slider to reflect new data x range
    function loadMassagedRealData(data, version) {
        ts = [] // clear data
        tStart = 0
        tEnd = 0

        // Track absolute weights. If input value is not a 'delta', need to track the absolute value running and calc
        var absWeight = Array(ydim*xdim);
        absWeight.fill(0);

        // Note that using a flat array for the snapshot greatly improves iteration speed
        var tempZ = Array(ydim*xdim);
        tempZ.fill(0);
        var tempZ2 = Array(ydim*xdim);
        tempZ2.fill(0);
        let snapCount = 0

        let buf = new BinaryBuffer(data);
        let reader 
        if (version == 1)
            reader = new BranchTraceReader(buf);
        else if (version == 2)
            reader = new BranchTraceReaderV2(buf)
        else if (version == 3)
            reader = new BranchTraceReaderV3(buf)
        let snapStart = 0
        let t = 0
        var snapshotPeriod = 200000;
        while (1) {
            let entry = reader.nextBranch()
            if (entry == null)
                break

            // Single record extracted 
            let table = entry[1]
            let row = entry[2]
            let weight = entry[3]
            let d_unique = entry[4]
            let thrash_1 = entry[5]
            let d_weight = 0
            let contrib_mpred = 0
            let contra_mpred = 0
            let weight_correct = 0
            let weight_incorrect = 0
            let pcLo = null
            let pcHi = null
            let correct = 0
            if (version >= 2) {
                d_weight = entry[6]
                contrib_mpred = entry[7]
                contra_mpred = entry[8]
                weight_correct = entry[9]
                weight_incorrect = entry[10]
                pcLo = entry[11]
                pcHi = entry[12]
                if (version >= 3) {
                    correct = entry[13]
                }
            }

            y = table
            x = row

            let value
            let value2 = null
            if (statIndex == -1) { // weight accuracy
                value = weight_correct
                value2 = (weight_correct + weight_incorrect)
            }
            else if (statIndex == -2) // num weight changes
                value = (weight != absWeight[y*xdim + x]) // any change in weight
            else if (statIndex == -3) { // harm
                value = contrib_mpred
                value2 = contrib_mpred + contra_mpred
            }
            else if (statIndex == -4) { // help
                value = contra_mpred
                value2 = contrib_mpred + contra_mpred
            }
            else if (statIndex == -5) { // mispred
                value = contrib_mpred + contra_mpred
            }
            else if (statIndex == -6) { // weight_change_thrash
                value = Math.abs(weight - absWeight[y*xdim + x]);
                value2 = weight - absWeight[y*xdim + x]
            }
            else if (statIndex == 0)
                value = 1
            else if (statIndex == 1)
                value = weight - absWeight[y*xdim + x]
            else if (statIndex == 3) // weight change mag
                value = Math.abs(weight - absWeight[y*xdim + x]);
            else
                value = entry[statIndex]

            absWeight[y*xdim + x] = weight

            if ((t % snapshotPeriod) == 0) {
                //logDebug(function(){return "snapshot
                snapEnd = t;
                ts.push([SNAPSHOT_TOKEN, snapStart, snapEnd, tempZ, snapCount, tempZ2]);
                console.log('snapshot ' + snapStart + ' end=' + snapEnd + ' count=' + snapCount)
                snapStart = t;
                tempZ = Array(ydim*xdim);
                tempZ.fill(0);
                tempZ2 = Array(ydim*xdim);
                tempZ2.fill(0);
                snapCount = 0

                // WARNING: skip an event this cycle just to keep each entries time matching its index for this prototype
                // TODO: this needs to be fixed and the "snapshot" needs to be stored in a separate structure
            }
            else {
                ts.push([POINT_TOKEN, t, x, y, 0,
                         value, value2,
                         pcLo, pcHi, correct]) // NOTE: this does not match shape of FAKE DATA!

                // TEMP: only tracking 1 counter for now. If more were needed, keep the array flat and index accordingly
                tempZ[y*xdim + x] += value
                if (value2 != null)
                    tempZ2[y*xdim + x] += value2
                snapCount++
            }

            t++
        }
        tEnd = t

        // Reset slider and stuff. Adjust range to fit. This needs some thought to adjust prdictable
        let slider = $(".js-range-slider").data('ionRangeSlider')
        let updates = {min: tStart, max: tEnd}
        if (slider.old_to > tEnd)
            updates['to'] = tEnd
        if (slider.old_from < tStart) {
            updates['from'] = tStart
        } else if (slider.old_from > tEnd) {
            updates['from'] = tStart // Old range was totally past new range
        }
        
        slider.update(updates)
        
    }


    function zeroZ() {
        z = [];
        for(const y of yValues) {
            z.push([]);
            zy = z[z.length - 1]
            for (const x of xValues) {
                zy.push(0);
            }
        }
        return z;
    }

    function renderBranchProfileList() {
        clearBranchFilters() // reset

        let bpm = new BranchProfileManager()

        let slider = $(".js-range-slider").data('ionRangeSlider')
        let t0 = slider.old_from
        let tEnd = slider.old_to

        // WARNING: assumes 0 or 1 event per cycle.
        for (var ti = tEnd-1; ti >= t0; ti--) {
            var tv = ts[ti];
            if(ti < 0) throw 'negative ti';
            if (ti >= ts.length) throw 'oob ti;'
            // WARNING:  this ignores timestamps for the moment
            var isSnapshot = Number(tv[0] == 1)
            if (isSnapshot) {
                // Skip snapshots. we want to real points only
            } else {
                // event
                let timestamp = tv[1]
                let x = tv[2];
                let y = tv[3];
                let c1 = tv[5];
                if (tv.length > 6) { // v2
                    let c2 = tv[6]
                    let pcLo = tv[7]
                    let pcHi = tv[8]
                    let correct = tv[9]

                    //let bp = new BranchProfile(pcLo, pcHi, 1) // This is exepensive
                    //bpm.mergeBranchProfile(bp)

                    bpm.addBranches(renderBranchAddress(pcLo, pcHi), 1, c1, c2, correct)
                }
            }
        }

        let tblStr = ''
        let bplist = bpm.getDescendingList() // by instances
        for (let bp of bplist) {
            /*<th>+filter</th>
            <th>pc</th>
            <th>count</th>
            <th>Stat1</th>
            <th>Stat2</th>
            <th>_</th>
            */
            let correctPredRate = (100. * bp.correct / bp.instances).toFixed(2) + '%'
            let idAdd = bp.pcStr + '-add-filter'
            let idRem = bp.pcStr + '-remove-filter'
            tblStr += '<tr>' + 
                      '<td>' + 
                      '<a id="' + idAdd + '" href="javascript:void(0);" onclick="addBranchFilter(\''+bp.pcStr+'\',\'' + idAdd + '\',\'' + idRem + '\');">[+]</a>' + 
                      '<a id="' + idRem + '" href="javascript:void(0);" onclick="remBranchFilter(\''+bp.pcStr+'\',\'' + idAdd + '\',\'' + idRem + '\');" style="display:none">[-]</a>' + 
                      '</td>' +
                      '<td>' + bp.pcStr + '</td><td>' + bp.instances + '</td>' +
                      '<td>' + bp.count1 + '</td><td>' + bp.count2 + '</td>' + 
                      '<td>' + correctPredRate + '</td>' + 
                      '</tr>'
        }

        let tbody = $('#branch-imem-tbody')[0]
        tbody.innerHTML = tblStr

        // TODO: combine stat 1 and 2.... for now just using first value for coloring?
    }

    function reproduceRowAccessTimeline() {
        Plotly.purge('rowAccessTimelineDiv');
        setTimeout(renderRowAccessTimeline, 1)
    }

    // Generate a row access timeline
    function renderRowAccessTimeline() {
        let slider = $(".js-range-slider").data('ionRangeSlider')
        let t0 = slider.old_from
        let tEnd = slider.old_to

        if (tEnd - t0 > 1000000) {
            alert('truncating window size to 1M pts')
            t0 = tEnd - 1000000
        }

        // WARNING: assumes 0 or 1 event per cycle.
        let rows = Array()
        let xs = Array()
        let vals = Array()
        for (var ti = tEnd-1; ti >= t0; ti--) {
            var tv = ts[ti];
            if(ti < 0) throw 'negative ti';
            if (ti >= ts.length) throw 'oob ti;'
            // WARNING:  this ignores timestamps for the moment
            var isSnapshot = Number(tv[0] == 1)
            if (isSnapshot) {
                // Skip snapshots. we want to real points only
            } else {
                // event
                let timestamp = tv[1]
                let x = tv[2];
                let y = tv[3];
                let c1 = tv[5];
                if (tv.length > 6) {
                    let c2 = tv[6]
                }

                // TODO: combine stat 1 and 2.... for now just using first value for coloring?

                let flatRowIdx = y*xdim + x // table * nrows = rowidx
                xs.push(ti) //xs.push(timestamp)
                rows.push(flatRowIdx)
                vals.push(c1)
            }
        }

        let rowTimelineLayout = {
          showlegend: false,
          title: 'Row timeline',
          //annotations: [],
          height: 800,
          margin: { t:0, l:40, r:0, b:40},
          xaxis: {
            autorange: true,
            title: 'branch#',
            ticks: '',
            side: 'bottom'
          },
          yaxis: {
            autorange: true,
            title: 'row (flat index)',
            ticks: '',
            ticksuffix: ' ',
            autosize: true
          }
        };

        // TODO: color rows per table?
        let rowTimelineData = [{
          x: xs,
          y: rows,
          type: 'scattergl',
          mode: 'markers',
          marker: {
            size: 1,
            color: vals,
            colorscale: colorScaleGradient,
          },
        }];

        Plotly.react('rowAccessTimelineDiv', rowTimelineData, rowTimelineLayout, {showSendToCloud: false});
    }
    

    // Regenerate Z from scratch
    // TODO: implement regenerate based on window deltas (.e.g add/subtract)
    // TODO: or... at least do not recreate z, just overwrite values with 0 and proceed
    var prevWinStart = 0
    var prevWinEnd = 0
    var prevZ = null
    var prevZ2 = null
    var lastZOut = null // latest data rendered
    var prevPerRowStats = null
    var prevPerRowStats2 = null
    var prevPerTableStats = null
    var prevPerTableStats2 = null
    var prevPoints = 0
    function regenerateZ(t, windowSize = 1000, forceRecalc=false) {
        var t0 = Math.max(tStart, t - windowSize);
        var tE = Math.min(tEnd, t);

        let z
        let z2
        let perTableStats
        let perRowStats
        let points = prevPoints
        if (prevZ == null || tE < prevWinStart || t0 > prevWinEnd || forceRecalc || branchFilters.size != 0) {
            // regenerate z data
            z = zeroZ();
            z2 = zeroZ();
            perTableStats = Array(ydim);
            perTableStats.fill(0)
            perTableStats2 = Array(ydim);
            perTableStats2.fill(0)
            perRowStats = Array(xdim);
            perRowStats.fill(0)
            perRowStats2 = Array(xdim);
            perRowStats2.fill(0)
            let skipSnapshots = branchFilters.size != 0
            points = processWindow(t0,tE,z,z2,perTableStats,perTableStats2,perRowStats,perRowStats2,1,skipSnapshots);
            console.log('processWindow case 1')
        } else {
            // some overlap, partially regenerate
            z = prevZ
            z2 = prevZ2
            perTableStats = prevPerTableStats
            perTableStats2 = prevPerTableStats2
            perRowStats = prevPerRowStats
            perRowStats2 = prevPerRowStats2

            // scale up mean aliasing to sum(aliasing) so it can be modified
            for (var i=0; i < perTableStats.length; i++){
                perTableStats[i] *= xdim; // number of rows
                perTableStats2[i] *= xdim; // number of rows
            }
            for (var i=0; i < perRowStats.length; i++){
                perRowStats[i] *= ydim; // number of tables
                perRowStats2[i] *= ydim; // number of tables
            }

            if (t0 < prevWinStart) {
                // add
                points += processWindow(t0,prevWinStart,z,z2,perTableStats,perTableStats2,perRowStats,perRowStats2,1);
                console.log('processWindow case 2')
            } else if (t0 >  prevWinStart) {
                // subtract
                points -= processWindow(prevWinStart,t0,z,z2,perTableStats,perTableStats2,perRowStats,perRowStats2,-1);
                console.log('processWindow case 3')
            }
            if (tE > prevWinEnd) {
                // add
                points += processWindow(prevWinEnd,tE,z,z2,perTableStats,perTableStats2,perRowStats,perRowStats2,1);
                console.log('processWindow case 4')
            } else if (tE < prevWinEnd) {
                // subtract
                points -= processWindow(tE,prevWinEnd,z,z2,perTableStats,perTableStats2,perRowStats,perRowStats2,-1);
                console.log('processWindow case 5')
            }
        }

        // TODO: Dividing these here and then taking the average in blocks below is not right
        for (var i=0; i < perTableStats.length; i++){
            perTableStats[i] /= xdim; // number of rows
            perTableStats2[i] /= xdim; // number of rows
        }
        for (var i=0; i < perRowStats.length; i++){
            perRowStats[i] /= ydim; // number of tables
            perRowStats2[i] /= ydim; // number of tables
        }

        // Calc new z by combining z,z2 (same with perTableStats)
        let out = zeroZ()
        let perTableStatsOut = Array(ydim);
        perTableStatsOut.fill(0)
        let perRowStatsOut = Array(xdim);
        perRowStatsOut.fill(0)
        if (statIndex == -6) { // weight_change_thrash
            // This should be sum(abs weight changes) / |sum(signed weight change)|
            // but *2 is not absolute. So apply abs to all final data sets
            let zmax = zmatDivAbs(z,z2,out)
            yvecDivAbs(perTableStats, perTableStats2, perTableStatsOut)
            xvecDivAbs(perRowStats, perRowStats2, perRowStatsOut)
        } else if (statIndex == -1 || statIndex == -3 || statIndex == -4) { // accuracy, harm, help,
            let zmax = zmatDiv(z,z2,out)
            yvecDiv(perTableStats, perTableStats2, perTableStatsOut)
            xvecDiv(perRowStats, perRowStats2, perRowStatsOut)
        } else {
            let zmax = zmatCopy(z,out)
            yvecCopy(perTableStats,perTableStatsOut)
            xvecCopy(perRowStats,perRowStatsOut)
        }

        prevWinStart = t0;
        prevWinEnd = tE;
        prevZ = z
        prevZ2 = z2
        prevPerTableStats = perTableStats
        prevPerTableStats2 = perTableStats2
        prevPerRowStats = perRowStats
        prevPerRowStats2 = perRowStats2

        // Update the heatmap distribution map with the largest n values
        let max = Array()
        let maxx = Array()
        let maxy = Array()
        let numToShow = 5
        for(const y of yValues) {
            zy = out[y]
            for (const x of xValues) {
                let v = zy[x]
                if (!isNaN(v)) {
                    if (max.length == 0 || v > max[max.length - 1]) {
                        max.push(v)
                        maxx.push(x)
                        maxy.push(y)
                        if (max.length > numToShow) {
                            max = max.slice(1,numToShow+1) // drop smallest
                            maxx = maxx.slice(1,numToShow+1)
                            maxy = maxy.slice(1,numToShow+1)
                        }
                    } else if (v > max[0]) {
                        // place in max where max[0] is smallest, max[numToShow-1] is largest
                        for (let i = 1; i < max.length; i++) {
                            if (v < max[i]) {
                                if (max.length == numToShow) {
                                    // Shift earlier down
                                    for (let j = 0; j < i-1; j++) {
                                        max[j] = max[j+1]
                                        maxx[j] = maxx[j+1]
                                        maxy[j] = maxy[j+1]
                                    }
                                    // insert
                                    max[i-1] = v
                                    maxx[i-1] = x
                                    maxy[i-1] = y
                                } else {
                                    // Push rest forward
                                    max.push(max[max.length-1]) // last to last+1
                                    maxx.push(maxx[maxx.length-1])
                                    maxy.push(maxy[maxy.length-1])
                                    for (let j = max.length-2; j >= i; j--) {
                                        max[j] =  max[j-1]
                                        maxx[j] =  maxx[j-1]
                                        maxy[j] =  maxy[j-1]
                                    }
                                    // insert
                                    max[i] = v
                                    maxx[i] = x
                                    maxy[i] = y
                                }
                                break
                            }
                        }
                    } else if (max.length < numToShow) {
                        max.splice(0,0,v)
                        maxx.splice(0,0,x)
                        maxy.splice(0,0,y)
                    }
                }
            }
        }

        let tableStr = ""
        for (let i = max.length - 1; i >= 0; i--) {
          let x = maxx[i]
          let y = maxy[i]
          let z = max[i]
          tableStr += '<tr><td><a href="javascript:void(0);" onclick="addTrackedCell('+y+','+x+');">[+]</a></td>' + 
                      '<td>' + y + '</td>' + 
                      '<td>' + x + '</td>' + 
                      '<td>' + Number(y%numBanks) + '</td>' + 
                      '<td>' + z + '</td></tr>' 
        }
        $('#top-cells-tbody')[0].innerHTML = tableStr


        lastZOut = out
        updateTrackedCellsTable()

        return [out,perTableStatsOut,perRowStatsOut,points];
    }

    // Divide a/b and place result in `out`.
    function zmatDiv(a,b,out) {
        let max = NaN
        for(const y of yValues) {
            ay = a[y]
            by = b[y]
            oy = out[y]
            for (const x of xValues) {
                oy[x] = ay[x] / by[x] // NOTE: may result in NaN
                //if (isFinite(oy[x]) && !isNaN(oy[x]) && oy[x] > max) { // store max finite value
                //    max = oy[x]
                //}
            }
        }
        return max
    }
    function zmatDivAbs(a,b,out) {
        let max = NaN
        for(const y of yValues) {
            ay = a[y]
            by = b[y]
            oy = out[y]
            for (const x of xValues) {
                oy[x] = Math.abs(ay[x] / by[x]) // NOTE: may result in NaN
                //if (isFinite(oy[x]) && !isNaN(oy[x]) && oy[x] > max) { // store max finite value
                //    max = oy[x]
                //}
            }
        }
        return max
    }
    function zmatCopy(a,out) {
        let max = NaN
        for(const y of yValues) {
            ay = a[y]
            oy = out[y]
            for (const x of xValues) {
                oy[x] = ay[x]
                //if (isFinite(oy[x]) && !isNaN(oy[x]) && oy[x] > max) { // store max finite value
                //    max = oy[x]
                //}
            }
        }
        return max
    }

    function yvecDiv(a,b,out) {
        for (const y of yValues) {
            out[y] = a[y] / b[y] // NOTE: may result in Nan
        }
    }
    function yvecDivAbs(a,b,out) {
        for (const y of yValues) {
            out[y] = Math.abs(a[y] / b[y]) // NOTE: may result in Nan
        }
    }
    function yvecCopy(a,out) {
        for (const y of yValues) {
            out[y] = a[y]
        }
    }

    function xvecDiv(a,b,out) {
        for (const x of xValues) {
            out[x] = a[x] / b[x] // NOTE: may result in Nan
        }
    }
    function xvecDivAbs(a,b,out) {
        for (const x of xValues) {
            out[x] = Math.abs(a[x] / b[x]) // NOTE: may result in Nan
        }
    }
    function xvecCopy(a,out) {
        for (const x of xValues) {
            out[x] = a[x]
        }
    }

    // Iterate data in window and add counters (multiplied by op) to the z and perTableStats arrays
    function processWindow(t0, tE, z, z2, perTableStats, perTableStats2, perRowStats, perRowStats2, op, skipSnapshots=false) {
        // WARNING: assumes 0 or 1 event per cycle.
        let snapSum = 0
        let pts = 0
        let snaps = 0
        for (var ti = tE-1; ti >= t0;) {
            var tv = ts[ti];
            if(ti < 0) throw 'negative ti';
            if (ti >= ts.length) throw 'oob ti;'
            // WARNING:  this ignores timestamps for the moment
            var isSnapshot = Number(tv[0] == 1)
            if (isSnapshot) {
                if (skipSnapshots) {
                    ti-- 
                    continue
                }

                snapStart = tv[1]
                snapEnd = tv[2]
                snapZ = tv[3]
                snapCount = tv[4]
                let snapZ2 = null
                if (tv.length >5)
                    snapZ2 = tv[5]

                if (snapStart >= t0) {
                    // Add this to bucket
                    if (ti == snapStart) {
                        // empty snapshot?
                        ti = -1;
                    } else
                        ti = snapStart;

                    snapZi = 0;
                    for(const y of yValues) {
                        zy = z[y]
                        z2y = z2[y]
                        for (const x of xValues) {
                            let v = snapZ[snapZi]
                            if (isNaN(v))
                                console.error('x=' + x + ' y=' + y + ' v=' + v)
                            let d = op*v
                            zy[x] += d
                            perTableStats[y] += d
                            perRowStats[x] += d

                            if (snapZ2 != null) {
                                let v2 = snapZ2[snapZi]
                                if (isNaN(v2))
                                    console.error('x=' + x + ' y=' + y + ' v=' + v2)
                                let d2 = op*v2
                                z2y[x] += d2
                                perTableStats2[y] += d2
                                perRowStats2[x] += d2
                            }

                            snapZi++
                            
                            // TEMP: only tracking one value for now
                            //values = snapZ[y*xdim + x];
                            //if (values == 0) {
                            //  // no data for this entry
                            //} else {f
                            //  v = values[0];
                            //  if (!Number.isInteger(v)) throw 'error:' + v + ' ' + x + ' ' + y + ' ' + values;
                            //  zy[x] += v; // TODO: all counters
                            //  perTableStats[y] += v;
                            //}
                        }
                    }
                    snaps++;
                    snapSum += snapCount
                } else {
                    ti--;
                }
            }
            else {
                // event
                let timestamp = tv[1]
                let x = tv[2];
                let y = tv[3];
                if (!Number.isInteger(x))
                    console.error('x=' + x)
                if (!Number.isInteger(y))
                    console.error('y=' + y)
                if (y > ydim)
                    console.error('y=' + y)
                if (x > xdim)
                    console.error('x=' + x)

                let c1 = tv[5];
                if (isNaN(c1))
                    console.error('c1=' + c1)

                let include = true
                if (tv.length > 6 && branchFilters.size > 0) {
                    let pcLo = tv[7]
                    let pcHi = tv[8]
                    let pcStr = renderBranchAddress(pcLo, pcHi)
                    include = branchFilters.has(pcStr)
                }

                if (include) {
                    let d = op*c1
                    z[y][x] += d; // TODO: all counters
                    perTableStats[y] += d;
                    perRowStats[x] += d
                    if (tv.length > 6) {
                        let c2 = tv[6]
                        if (isNaN(c2))
                            console.error('c2=' + c2)
                        let d2 = op*c2
                        z2[y][x] += d2; // TODO: all counters
                        perTableStats2[y] += d2;
                        perRowStats2[x] += d2
                    }
                }
                pts++;
                ti--;
            }
        }

        logDebug(function(){return 'processWindow ' + op + ' (' + t0 + '-' + tE + ') => ' + pts + ' pts, ' + snaps + ' snaps';});
        return pts + snapSum

    };

    var datarevision = 0;
    function render(zValues, perTableStats, perRowStats) {
        let statName = statIndexReverseMap[statIndex]
        rowDataLayout['title'] = statName

        datarevision++; // sometimes this uses all the same data arrays with updated values

        var data = [{
          x: xValues,
          y: yValues,
          z: zValues,
          type: 'heatmap', //'heatmapgl' causes blurring of cells and does not support ygap. use 'heatmap'
          colorscale: colorScaleGradient,
          ygap: 1,
          // xgap: 1, // miss things when zoomed out fully. Looks great when zoomed in though.
          //showscale: false
        }];

        layout['datarevision'] = datarevision;
        Plotly.react('heatmapDiv', data, layout, {showSendToCloud: false});

        // TODO: only add this listener once... not every render!
        $('#heatmapDiv')[0].on('plotly_click', function(data) {
            if (data.points.length == 0)
                return

            let pt = data.points[0]
            let rowx = pt.x
            let tabley = pt.y

            addTrackedCell(tabley, rowx)
        });

        var tableData = {
            x: perTableStats,
            y: yValues,
            type: 'line',
            colorscale: colorScaleGradient
        }

        var dummyData = {
            x: [0],
            y: [0],
            type: 'markers'
        }


        // TODO: these should be subplots
        tableDataLayout['datarevision'] = datarevision;
        Plotly.react('perTableChartDiv', [tableData, dummyData], tableDataLayout, {showSendToCloud: false});

        var tableData = {
            x: xValues,
            y: perRowStats,
            type: 'scattergl', //type: 'bar' is intense to draw
            mode: 'lines',
            //marker: {
            //    size:4,
            //    colorscale: colorScaleGradient,
            //}
            line: {
                width: 0.5,
            }
        }

        // TODO: these should be subplots
        rowDataLayout['datarevision'] = datarevision;
        Plotly.react('perRowChartDiv', [tableData], rowDataLayout, {showSendToCloud: false});
    }

    var rangeTimeEl = null;

    function updateHeatmapFromSlider() {
        var el = document.getElementById("my_range");
        vals = el.value.split(';');
        from = Number(vals[0]);
        to = Number(vals[1]);
        updateHeatmap(to, to - from, true /* force recalc */);
    }

    function updateHeatmap(t, windowSize, forceRecalc=false) {
        $('#window_size')[0].value = windowSize;

        t0 = performance.now();
        var result = regenerateZ(t, windowSize, forceRecalc);
        z = result[0]
        perTableStats = result[1]
        perRowStats = result[2]
        pts = result[3]
        t1 = performance.now();
        render(z, perTableStats, perRowStats);
        t2 = performance.now();
        logDebug(function(){return 'updated w/ time=' + t + ' win=' + windowSize + ' collect:' + (t1-t0) + ' render:' + (t2-t1) + ' pts:' + pts;});
    }

    // Generate the timeline graph of the selected statistic `statIndex`
    // Iterates the data to produce it so call this sparingly
    function renderTimeline() {
        let statName = statIndexReverseMap[statIndex]

        var timelineDataLayout = {
            showlegend: true,
            title: '',
            height: 160,
            margin: { t:0, 
                      l:40, // roughly match td to the left of textbox
                      r:120, // roughly match window_size textbox
                      b:30},
            xaxis: {
                autorange: false,
                range: [tStart,tEnd],
                title: 'mean ' + statName,
                ticks: '',
                side: 'bottom'
            },
            yaxis: {
                autorange: true,
                title: 'table index',
                ticks: '',
                ticksuffix: ' ',
                autosize: true
            }
        };

        let xs = [];
        let ys = [];
        let yPerTable = [...Array(ydim)]; // ydim is just num tables, which was 'y' in heatmap but is something else here
        yPerTable = yPerTable.map(function(o) { return [];});
        let yPerTableBucketSum = Array(ydim);
        yPerTableBucketSum.fill(0);
        let yPerTableBucketCount = Array(ydim);
        yPerTableBucketCount.fill(0);
        let bucketCount = 0;
        let bucketSum = 0;
        let bucketSize = 4999; // Just trying not to collide with snapshots
        if (tEnd - tStart < 5000) {
            bucketSize = 1
        } else if (tEnd - tStart < 100000) { 
            bucketSize = Math.floor((tEnd - tStart) / 1000)
        }
        for (let i = tStart; i < tEnd; i++) {
            var tv = ts[i];
            // WARNING:  this ignores timestamps for the moment
            var isSnapshot = Number(tv[0] == 1);
            if (!isSnapshot) {
                var x = tv[2];
                var y = tv[3];
                var c1 = tv[5];
                bucketCount += 1;
                bucketSum += c1;
                
                yPerTableBucketCount[y] += 1;
                yPerTableBucketSum[y] += c1;
            }

            if ((i % bucketSize) == 0) {
                xs.push(i); // i is currently a proxy for t in this prototype
                if (bucketCount == 0) {
                    ys.push(0);
                } else {
                    ys.push(bucketSum/bucketCount);
                }
                bucketCount = 0;
                bucketSum = 0;

                for (let j = 0; j < yPerTable.length; j++) {
                    if (yPerTableBucketCount[j] == 0)
                        yPerTable[j].push(0);
                    else
                        yPerTable[j].push(yPerTableBucketSum[j]/yPerTableBucketCount[j]);
                    yPerTableBucketSum[j] = 0
                    yPerTableBucketCount[j] = 0;
                }
            }
        }

        var timelineData = []
        let lineWidth = 0.2 // thickness of lines (<1 implies transparency)
        for(let tbl=0; tbl < ydim; tbl++) {
            timelineData.push({name: 'Table ' + tbl, x: xs, y: yPerTable[tbl], type: 'lines', line: { width:lineWidth }});
        };
        timelineData.push({
            name: 'Mean',
            x: xs,
            y: ys,
            type: 'line',
        });

        Plotly.react('timelineDiv', timelineData, timelineDataLayout, {showSendToCloud: false});
    }

    // Generate the mispredict timeline graph
    // Iterates the data to produce it so call this sparingly.
    // This does not change based on selected stat, so this should only need to be
    function renderMispredictRate() {
        let statName = statIndexReverseMap[statIndex]

        var mpTimelineDataLayout = {
            showlegend: true,
            title: '',
            height: 160,
            margin: { t:0, 
                      l:40, // roughly match td to the left of textbox
                      r:120, // roughly match window_size textbox
                      b:30},
            xaxis: {
                autorange: false,
                range: [tStart,tEnd],
                title: 'mispredict rate',
                ticks: '',
                side: 'bottom'
            },
            yaxis: {
                autorange: true,
                title: 'table index',
                ticks: '',
                ticksuffix: ' ',
                autosize: true
            }
        };

        let xs = [];
        let ys = [];
        let bucketCount = 0;
        let bucketSum = 0;
        let bucketSize = 4999; // Just trying not to collide with snapshots
        if (tEnd - tStart < 5000) {
            bucketSize = 1
        } else if (tEnd - tStart < 100000) { 
            bucketSize = Math.floor((tEnd - tStart) / 1000)
        }
        for (let i = tStart; i < tEnd; i++) {
            var tv = ts[i];
            // WARNING:  this ignores timestamps for the moment
            var isSnapshot = Number(tv[0] == 1);
            if (!isSnapshot) {
                var x = tv[2];
                var y = tv[3];
                var c1 = tv[5];
                let mpred
                if (tv.length > 6) {
                    mpred = !tv[9]
                }
                else {
                    mpred = 0
                }
                bucketCount += 1;
                bucketSum += mpred;
            }

            if ((i % bucketSize) == 0) {
                xs.push(i); // i is currently a proxy for t in this prototype
                if (bucketCount == 0) {
                    ys.push(0);
                } else {
                    ys.push(bucketSum/bucketCount);
                }
                bucketCount = 0;
                bucketSum = 0;
            }
        }

        var mpData = []
        let lineWidth = 0.2 // thickness of lines (<1 implies transparency)
        mpData.push({
            name: 'Mean',
            x: xs,
            y: ys,
            type: 'line',
        });

        Plotly.react('mpDiv', mpData, mpTimelineDataLayout, {showSendToCloud: false});
    }

    // Upon loading new data, perform some updates and redraws
    function doPostDataLoad() {
        renderMispredictRate();
        renderTimeline();
        logDebug(function(){return 'generating z for window...';});
        setTimeout(function() {
            updateHeatmapFromSlider()

            $('#branchImemTable')[0].style.visibility = 'visible';
            $('#topCellsTable')[0].style.visibility = 'visible';
            $('#trackedCellsTable')[0].style.visibility = 'visible';
        }, 1);
    }

    function initRangeSlider(minval, maxval) {
        let range = maxval - minval
        $(".js-range-slider").ionRangeSlider({
            type: 'double',
            skin: 'big', // 'round',
            grid: true,
            grid_snap: false,
            //grid_cells: 10,
            grid_num: 10,
            min: tStart,
            max: tEnd,
            from: Math.max(0, tEnd - Math.min(range, 600000)),
            to: tEnd,
            drag_interval: true,
            min_interval: 100,
            max_interval: null,
            onChange: function (data) {
                if (ts != null) // ensure data is ready
                    updateHeatmap(data.to, data.to-data.from, false /* no force recalc */);
            },
            onUpdate: function (data) {
                if (ts != null) // ensure data is ready
                    updateHeatmap(data.to, data.to-data.from, false /* no force recalc */);
            }

        });
        $( ".widget input[type=submit], .widget a, .widget button" ).button();
        $(".js-range-slider").click( function (event) { // stop this slider from handling html 'input' element clicks
          event.preventDefault();
        } );
        jQuery('.integer-text').keyup(function () { 
        this.value = this.value.replace(/[^0-9]/g,'');
        });
        jQuery('.float-text').keyup(function () { 
            this.value = this.value.replace(/[^0-9\.]/g,'');
        });
    }

    $( function() {
        initRangeSlider(tStart,tEnd)
    });


    var stopAnimationFlag = false
    function animationStep(stepSize, stepsPerSecond) {
        let slider = $('.js-range-slider').data('ionRangeSlider')

        let range = slider.old_to - slider.old_from
        let from = slider.old_from + stepSize;
        let to = slider.old_to + stepSize;

        if (to > tEnd) {
            to = tEnd
            from = to - range
        }

        slider.update({from: from, to: to});

        if (to == tEnd)
            stopAnimation();

        // TEMP:
        ////stopAnimation();
        if (!stopAnimationFlag) {
            // TODO: This does not take into account how long the animation step took to.
            //       Needs to track last frame start time.
            let delayMs = 1000.0 / stepsPerSecond;
            setTimeout(function() { animationStep(stepSize, stepsPerSecond) }, delayMs);
        }
    }

    function beginAnimation() {
        var stepSize = Number($('#cycles_per_step')[0].value);
        if (stepSize <= 0) alert('Stepsize must be > 0');
        var stepsPerSecond = Number($('#steps_per_second')[0].value);
        if (stepsPerSecond <= 0) alert ('Steps/second must be > 0');

        $('#animate')[0].style.display = 'none';
        $('#stop')[0].style.display = 'inline-block';

        let slider = $('.js-range-slider').data('ionRangeSlider')
        if (slider.old_to == tEnd) {
            slider.update({from: tStart, to: tStart + (slider.old_to - slider.old_from)});
        }

        stopAnimationFlag = false;
        animationStep(stepSize, stepsPerSecond);
    };

    function stopAnimation() {
        $('#animate')[0].style.display = 'inline-block';
        $('#stop')[0].style.display = 'none';

        stopAnimationFlag = true;
    };

    function onWindowTextEdited() {
        // user changed this
        let txt = $('#window_size')[0];
        txt.style.fontStyle = 'italic';
        txt.style.color = '#a0a0a0';
        txt.style.fontWeight = '';
    };

    function onWindowTextVerified() {
        let txt = $('#window_size')[0];
        txt.style.fontStyle = '';
        txt.style.color = '#000000';
        txt.style.fontWeight = 'bold';
    }

    function onWindowTextChange(value) {
        let r = Number(value);
        if (r <= 0) alert ('enter a valid window size value > 0, not ' + r);

        let slider = $('.js-range-slider').data('ionRangeSlider')
        let old_range = slider.old_to - slider.old_from;
        
        if (r > old_range) {
            // grow
            let grow = r - old_range;
            let new_to
            let new_from

            // inrcrease the 'to' first
            if (slider.old_to + grow > tEnd) {
                grow -= tEnd - slider.old_to;
                new_to = tEnd;
            } else {
                new_to = slider.old_to + grow;
                grow = 0;
            }

            if (grow > 0) {
                if (slider.old_from - grow < tStart) {
                    new_from = tStart;
                    grow -= slider.old_from - tStart;
                } else {
                    new_from = slider.old_from - grow;
                    grow = 0;
                }
            } else {
                new_from = slider.old_from
            }

            if (grow > 0) {
                $('#window_size')[0].value = r - grow;
            }

            // Update slider to reflect range. This will upate the heatmap
            slider.update({from: new_from, to: new_to});

        } else if (r < old_range) {
            // shrink

            // only shrink the "from" so that "current cycle" doesn't change
            let new_from = slider.old_from + (old_range - r);
            slider.update({from: new_from, to: slider.old_to});
        }
        
        onWindowTextVerified();
    };

    function onCheckChange(box) {
        // TODO:
    }

    function addBranchFilter(pcStr, idAdd, idRem) {
        branchFilters.set(pcStr, 1)
        $('#' + idAdd)[0].style.display='none';
        $('#' + idRem)[0].style.display='inline-block';
        updateHeatmapFromSlider()
    }

    function remBranchFilter(pcStr, idAdd, idRem) {
        $('#' + idAdd)[0].style.display='inline-block';
        $('#' + idRem)[0].style.display='none';

        if (!branchFilters.has(pcStr))
            return // not present.

        branchFilters.delete(pcStr)
        updateHeatmapFromSlider()
    }

    function clearBranchFilters() {
        if (branchFilters.size == 0)
            return // already empty

        branchFilters = new Map()
        updateHeatmapFromSlider()
    }

    function addTrackedCell(tabley,rowx) {
        for (let i = 0; i < trackedCells.length; i++) {
            if(trackedCells[i][0] == rowx && trackedCells[i][1] == tabley) {
                return // already present
            }
        }
        trackedCells.push([rowx,tabley])
        updateTrackedCellsTable()
    }
    function removeTrackedCell(tabley,rowx) {
        for (let i = 0; i < trackedCells.length; i++) {
            if(trackedCells[i][0] == rowx && trackedCells[i][1] == tabley) {
                trackedCells.splice(i,1) 
                break
            }
        }
        updateTrackedCellsTable()
    }

    // Repopulate the tracked cells table based on the `trackedCells` data structure
    function updateTrackedCellsTable() {
        let tableStr = ""
        for (let i = 0; i < trackedCells.length; i++) {
            let rowx = trackedCells[i][0]
            let tabley = trackedCells[i][1]
            let z = lastZOut[tabley][rowx]

            // TODO: onChange support?
            tableStr += '<tr><td><input type="checkbox" value="checked" /></td>' + 
                        '<td>' + tabley + '</td>' + 
                        '<td>' + rowx + '</td>' + 
                        '<td>' + Number(y%numBanks) + '</td>' + 
                        '<td style="background-color:#ff4040;">' + z + '</td>' + 
                        '<td><a href="javascript:void(0);" onclick="removeTrackedCell('+tabley+','+rowx+');">[x]</a></td></tr>'
        }
        $('#tracked-cells-tbody')[0].innerHTML = tableStr
    }


    // Generate some collapsible content for the call-stack table
    // This is just an example
    function buildCallStackTable() {
        var jsTT = com_github_culmat_jsTreeTable

        ////jsTT.treeTable($('#callStack'));

        var data = [
            {
              id : 'Function A',
              inclusive : '80%',
              mp_rate_excl : ''
            }, {
              id : 'Function B',
              inclusive : '20%',
              mp_rate_excl : '.01%',
              mp_rate_excl : ''
            }, {
              id : 'Function C',
              inclusive : '30%',
              parent : 'Function A',
              mp_rate_excl : ''
            }, {
              id : 'Function D',
              inclusive : '70%',
              parent : 'Function A',
              mp_rate_excl : '.4%'
            }, {
              id : 'Function E',
              inclusive : '10%',
              parent : 'Function C',
              mp_rate_excl : '1.1%'
            }, {
              id : 'Function F',
              inclusive : '90%',
              parent : 'Function C',
              mp_rate_excl : '.4%'
            }]
                
        var tree = jsTT.makeTree(data);
        var rendered = jsTT.renderTree(tree, 'children', 'id', {'id':'Name','inclusive':'instances(inc)', 'mp_rate_excl':'mp%(self)'}, null, {'class':'callStackTable'});
        var final = jsTT.treeTable(rendered);
        //final.insertAfter($('#treetable-rendered'))
        //final.replace($('#callStackDiv'));
        ////let div = $('#callStackDiv');
        ////div[0].innerHTML = final[0].outerHTML;
        let prev = $('callStackTableElement');
        if (prev.length)
            prev.remove();
        //final.insertAfter($('#callStackDiv'))

        $('#callStackDiv')[0].innerHTML = final[0].outerHTML
    }

    // Takes a file from a FileList object's selection handler
    // Sets global currentFile
    function loadNewFile(file) {
        currentFile = file
        let reader = new FileReader();
        let version = 1 
        if (file.name.includes('.V2.')) // use filename to figure out file format version (experimenting)
            version = 2
        if (file.name.includes('.V3.')) // use filename to figure out file format version (experimenting)
            version = 3
        reader.onload = function(e) {
            try {
                let data = e.target.result
                loadMassagedRealData(data, version)
                doPostDataLoad()
            } finally {
                hideBusyBox()
            }
        }
        showBusyBox()
        reader.readAsBinaryString(file);
    }

    function handleFileSelect(evt) {
        var files = evt.target.files; // FileList object
        if (files.length == 0)
            return

        // files is a FileList of File objects. List some properties.
        //var output = [];
        //for (var i = 0, f; f = files[i]; i++) {
        //  output.push('<strong>', escape(f.name), '</strong> (', f.type || 'n/a', ') - ',
        //              f.size, ' bytes, last modified: ',
        //              f.lastModifiedDate ? f.lastModifiedDate.toLocaleDateString() : 'n/a');
        //}
        //document.getElementById('current-filename').innerHTML = output.join('');

        let f = files[0]
        $('#current-filename').innerHTML = escape(f.name) + ' (' + f.size + ' bytes)'
        
        loadNewFile(f)
    }

    // DOM is ready to go. This is like onLoad.
    $(document).ready( function() {
        $("#file").on('change', handleFileSelect);

        // Init treeTable stuff
        var jsTT = com_github_culmat_jsTreeTable
        // register on the window object
        com_github_culmat_jsTreeTable.register(this)

        // Set up initial data
        dbgEl = $('#debugDiv')[0];
        setTimeout(function() {
            logDebug(function(){return 'generating random data...';});
            showBusyBox()
            setTimeout(function() {
                try {
                    ts = generateRandomData();
                    doPostDataLoad()
                } finally {
                    hideBusyBox()
                }
            }, 1);
        }, 1);
    } );

    // User selected a new statistic (maybe) from the select box
    function selectNewStatistic(selected_options) {
        if (selected_options.length == 0)
            return

        let value = selected_options[0].value
        console.log(value)

        statIndex = statIndexMap[value] // assign global

        if (currentFile == null)
            return // nothing new to load

        loadNewFile(currentFile)
    }

    function showBusyBox() {
        $('#busy-div')[0].style.display = "table"
    }

    function hideBusyBox() {
        $('#busy-div')[0].style.display= "none"
    }

  </script>
</head>

<body>
  <div id="busy-div" style="z-index:100; display:none; background: rgba(200,200,200,0.9); position:absolute; width:100%; height:100%; top: 0; right: 0; bottom: 0; left: 0; text-align: center;">
    <span style="display:table-cell; vertical-align: middle;">
        <h2>Processing...</h2>
        <em>prototype: if this mesage persists for more than 10 seconds with under 5 million data points, check console errors and restart</em>
    </span>
  </div>
  <div class="prototype-warning">PROTOTYPE / PROOF OF CONCEPT</div>
  <div class="prototype-warning">TESTED ON CHROME ONLY</div>
  <div style="display:inline-block;">
    <form method="post" enctype="multipart/form-data">
        <em>Data:</em> <input type="file" id="file" />
        <output id="current-filename"></output>
        &nbsp;&nbsp;&nbsp;<em>Stat:</em><select onchange="selectNewStatistic(this.selectedOptions)">
            <option value="accesses">Write Accesses</option>
            <option value="weight_change">Weight Change</option>
            <option value="weight_change_mag">Weight Changes Absolute Sum</option>
            <option value="weight_change_thrash">Weight Thrashing</option>
            <option value="weight_change_events">Weight Update Events</option>
            <option value="thrash_1">Write Thrashing (History length of 1)</option>
            <option value="unique_pcs">Unique Write PCs (first seen within window only)</option>
            <option value="contrib_mispred">Row Weight Contributed to a SHP Mispredict</option>
            <option value="contra_mispred">Row Weight Contradicted a SHP Mispredict</option>
            <option value="mispred">Part of a Mispredict [stat not quite accurate]</option>
            <option value="mp_harm_rate">Row Weight Mispredict Contribute Rate (HARMFUL)</option>
            <option value="mp_help_rate">Row Weight Mispredict Contradict Rate (HELPFUL)</option>
            <option value="weight_mpreds">Row Weight Alone would be a Misprediction (ignore bias)</option>
            <option value="weight_cpreds">Row Weight Alone would be a Correct Prediction (ignore bias)</option>
            <option value="weight_accuracy">Row Weight Alone Prediction Accuracy (ignore bias)</option>
            <option value="">Write Thashing (History length of 2) [NOT AVAILABLE]</option>
            <option value="">Write Thashing (History length of 5) [NOT AVAILABLE]</option>
            <option value="">Write Thashing (History length of 10) [NOT AVAILABLE]</option>
            <option value="">Read Accesses [NOT AVAILABLE]</option>
        </select>
        <div class="prototype-warning-small">&lt;&lt;Will be able to combine these selcetable stats with simple arithmetic!</div>
    </form>
  </div>
  <div class="slidecontainer" style="width:100%;">

    <!-- upper section with heatmap -->
    <table style="width:100%" cellpadding=0 cellspacing=0>
        <tbody>
            <tr>
                <td style="width:180px; heigth:100px;">
                    <!--dead space in UL corner-->
                    <div style="padding:2px; border:1px solid #c0c0c0; margin-right:10px;"> 
                        <button class="ui-button ui-widget ui-corner-all" id="animate" onclick="beginAnimation();" style="width:100%; margin-bottom:2px" >Animate</button>
                        <button class="ui-button ui-widget ui-corner-all" id="stop" onclick="stopAnimation();" style="display:none; width:100%; margin-bottom:2px" >Stop</button><br/>
                        <input type="text" class="integer-text ui-widget ui-corner-all" id="cycles_per_step" value="500" style="width:50px; font-size:12px"/>
                        branches/step<br/>
                        <input type="text" class="float-text ui-widget ui-corner-all" id="steps_per_second" value="5.0" style="width:50px; font-size:12px" /> steps/second
                        <!--<span class="note">(depending on the window size, this may be verry clunky)</span><br/>-->
                    </div>
                </td>
                <td>
                    <div id="perRowChartDiv" style="width:100%;"></div>
                </td>
            </tr>
            <tr>
                <td style="width:180px; height:350px;">
                    <div id="perTableChartDiv"></div>
                </td>
                <td>
                    <div id="heatmapDiv"></div>
                </td>
            </tr>
        </tbody>
    </table>

    <!-- lower section with graphs and slider -->
    <div style="float:left; width:70%; padding:0px; box-sizing:border-box; min-height:200px;">
        <!-- timeline graphs -->
        <div style="width:100%; height:160px; border-top:1px solid black;">
            <div id="timelineDiv"><!-- Plotly chart will be drawn inside this DIV --></div>
        </div>
        <div style="width:100%; height:160px; border-top:1px solid black;">
            <div id="mpDiv"><!-- Mispredict rate? --></div>
        </div>
        <!-- slider: inside its own table to deal with jitter on layout caused by the slider itself-->
        <table style="width:100%" cellpadding=0 cellspacing=0>
            <tbody>
                <tr>
                    <td style="width:30px;">
                    <td>
                        <input type="text" class="js-range-slider" id="my_range" value=""/>
                    </td>
                    <td style="width:100px; text-align:right;">
                        window<br/>
                        <input type="text" class="integer-text ui-widget ui-corner-all" id="window_size" value="-" style="margin-top:0px; width:80px;" onchange="onWindowTextChange(this.value);" oninput="onWindowTextEdited();" />
                    </td>
                </tr>
            </tbody>
        </table>
    

        <div style="border:1px solid grey; padding:10px">
            <input type="button" onClick="reproduceRowAccessTimeline();" value="Generate Timeline (slow)">
            <div id="rowAccessTimelineDiv"></div>
        </div>
        <div style="border:1px solid grey; padding:10px">
            Should be able to mark ranges here somehow as reminders of interesting regions?
        </div>
        <div style="border:1px solid grey; padding:10px">
            An auto-zoomed view of the timeline plot should replace it. The non-zoomed timeline plot belongs here.
        </div>
        <div style="border:1px solid grey; padding:10px">
            Flame-graph? Or maybe a function context graph: A simple plot of "location" that could include a hashes of portions of call stack and depth
        </div>
    </div>

    <!-- right-side panel -->
    <div style="float:left; width:30%; min-height:200px">
        <!-- panels and profiling -->
        <div style="border:1px solid #808080; padding:8px;">
            <span style="font-size:18px; font-family:Arial; letter-spacing:1px;">Top Cells</span>
             <table id="topCellsTable" style="width:100%; margin-top:10px; font-family:monospace; text-align:right; visibility:hidden cellpadding=0 cellspacing=0">
                <thead>
                    <tr style='background-color:#c0c0f0;'>
                        <th>Track</th>
                        <th>Table (incl. bank)</th>
                        <th>Row</th>
                        <th>Bank</th>
                        <th>Value</th>
                    </tr>
                </thead>
                <tbody id="top-cells-tbody">
                </tbody>
            </table>
        </div>
        <div style="border:1px solid #808080; padding:8px;">
            <span style="font-size:18px; font-family:Arial; letter-spacing:1px;">Tracked Cell(s)</span><em></em>
            <table id="trackedCellsTable" style="width:100%; margin-top:10px; font-family:monospace; text-align:right; visibility:hidden">
                <thead>
                    <tr style='background-color:#c0c0f0;'>
                        <th>show</th>
                        <th>Table (incl. bank)</th>
                        <th>Row</th>
                        <th>Bank</th>
                        <th>Value</th>
                        <th>X</th>
                    </tr>
                </thead>
                <tbody id="tracked-cells-tbody">
                </tbody>
            </table>

            <!--
            <div class="prototype-warning-small">This box is for tracking cells throughout time and seeing some concrete statistics.<br/>
            For example: it may show
            <li> A table of all stats for that cell over the selected time range</li>
            <li> A list of branches including opcodes which touch them</li>
            Cells tracked in this box will have one the current selected 'stat' displayed on the timeline chart. It may also be highlighted in the heatmap? Maybe the heatmap color should be displayed beside each row<br/>
            There may be a limit of 'enabled' tracked cells.
            </div>
            -->
        </div>
        <!-- Code information -->
        <div style="border:1px solid #808080; padding:8px;">
            <span style="font-size:18px; font-family:Arial; letter-spacing:1px;">Branch Imem</span><em>(within selection)</em> <input type="button" onClick="renderBranchProfileList();" value="generate" />
            <!--input type="button" onclick="clearBranchFilters();" value="clear branch filters" />-->
            <table id="branchImemTable" style="width:100%; margin-top:10px; font-family:monospace; text-align:right;visibility:hidden" cellpadding=0 cellspacing=0>
                <thead>
                    <tr style='background-color:#c0c0f0;'>
                        <th>+filter</th>
                        <th>pc</th>
                        <th>count</th>
                        <th>Stat1</th>
                        <th>Stat2</th>
                        <th>Correct%</th>
                    </tr>
                </thead>
                <tbody id="branch-imem-tbody">
                </tbody>
            </table>
            <!--
            <div class="prototype-warning-small">This box will contain a of the BRANCHES seen within this window ranked by some metric and some of their information and statistics. Selecting a branch here should cause it to appear in the call stack box below and potentially could filter the 'top cells' list to include cells effecting that branch</div>
            -->
        </div>
        <!-- TODO: Add a call-stack display -->
        <!--<div style="border:1px solid #808080; padding:8px;">
            <span style="font-size:18px; font-family:Arial; letter-spacing:1px;">Stack</span><em>(of selected branches)</em>
            <div id="callStackDiv"></div>
        </div>-->
        <div style="border:1px solid #808080; padding:8px;">
            <span style="font-size:18px; font-family:Arial; letter-spacing:1px;">Heatmap Distributions</span>
            <div class="prototype-warning-small">This should contain some histograms (per table) showing distribution of heatmap values. Similarly, a few percentile lines could be drawn on the line-plot to the very left to capture similar information</div>
        </div>
    </div>    
  </div>
  <hr>
  <strong>DEBUG:</strong>
  <div id="debugDiv" style="height:500px; overflow:scroll;">Loading...</div>
  <script>

    
    
  </script>
</body>